# vim:ts=4:sw=4:noet:
""" VM remote interface """

import logging
from simpleobj import SimpleObj, SimpleSmallInt

ERROR_BAD_MSG     = 7
ERROR_RUNTIME     = 14

NIL_PTR = 0x4000

errNames = [
	"Unknown error",
	"Halt instruction",
	"Fatal primitive error",
	"Out of memory",
	"Root stack overflow",
	"Root stack underflow",
	"Stack underflow",
	"Method not understood",
	"Static object write",
	"Object data out of range",
	"Communication message overflow",
	"Tried to convert non-bin to string",
	"Invalid bytecode instruction",
	"Static cache overflow",
	"Runtime error",
]

class RemoteStackFrame:
	""" Representation of a remote stack frame (from L{Remote.backtrace()}) """
	def __init__(self, meth, incls, recv, line, type, args, temps):
		self.meth = meth
		self.incls = incls
		self.recv = recv
		self.line = line
		self.type = type
		self.args = args
		self.temps = temps

class RemoteObj:
	""" L{SimpleObj} wrapper for backtrace views """
	def __init__(self, value, ptr=None):
		self.value = value
		self.ptr = ptr
		if self.ptr:
			self.loc = "%s@%s" % (str(self), self.ptr)
		else:
			self.loc = str(self)

	def __repr__(self):
		return self.value

	def __str__(self):
		return str(self.value)

class Remote:
	def __init__(self, dm, status):
		self.globals = None
		self.dm = dm
		self.conn = None
		self.status = status

	def send(self,command,data, showresp=1):
		msg = ""
		msg += chr(len(data)+3)
		msg += chr(command)
		for d in data:
			msg += chr(d)
		msg += '\0'
		self.conn.send(msg)
		r = map(lambda x: ord(x), self.conn.recv())
		if len(r)==0:
			raise Exception("Timeout sending data to robot")
		if len(r)<3 or len(r)!=r[0]:
			raise Exception("Invalid response from robot: %s" % r)
		else:
			resp_len = r[0]
			resp = r[1:-1]
			if showresp:
				self.parseResp(resp)
			return resp

	def parseResp(self, resp):
		cmd = resp[0]
		data =  resp[1:]
		if cmd > 1:
			return
		self.status.printNnl(" Response:",)
		if cmd==1:
			if data[0]==1:
				self.status.printNl("OK")
			else:
				self.status.printNl("ERROR")
		else:
			self.status.printNl("Unknown")
	
	def parseObject(self, data):
		truncated = data[0]==5
		if len(data)<4:
			self.status.printNl("Invalid object")
			return
		ptr = (data[2]<<8) + data[1]
		if ptr&2:
			return SimpleSmallInt(ptr>>2)
		hdr = (data[4]<<8) + data[3]
		bin = hdr&0x8000
		hdr &= (~0x8000)
		size = hdr>>2
		cls = (data[6]<<8) + data[5]
		odata = []
		if bin:
			if (size+7)>len(data):
				if truncated:
					for i in range(len(data)-7):
						odata.append(data[7+i])
				else:
					raise Exception("Invalid bin object size: %d" % size)
			else:
				for i in range(size):
					odata.append(data[7+i])
		else:
			if (size*2+7)>len(data):
				if truncated:
					for i in range((len(data)-7)>>1):
						odata.append((data[8+(i*2)]<<8) + data[7+(i*2)])
				else:
					raise Exception("Invalid object size: %d" % size)
			else:
				for i in range(size):
					odata.append((data[8+(i*2)]<<8) + data[7+(i*2)])
		return SimpleObj(self, ptr, cls, size, bin, odata, truncated)

	def parseDict(self, dict):
		keys = self.getObject(dict.data[0])
		vals = self.getObject(dict.data[1])
		pdict = {}
		for i, k in enumerate(keys.data):
			key = self.getObject(k).asString()
			val = self.getObject(vals.data[i])
			pdict[key] = val
		return pdict

	def detect(self):
		pass

	def setVMState(self, state):
		self.dm.aquireConnection()
		self.send(1,[state], showresp = 0)
		self.dm.releaseConnection()

	def getVMState(self):
		self.dm.aquireConnection()
		s = self.send(2,[])
		self.dm.releaseConnection()
		return s

	def restart(self):
		self.dm.aquireConnection()
		self.send(3,[], showresp = 0)
		self.dm.releaseConnection()

	def getObject(self, optr):
		self.dm.aquireConnection()
		if optr&2:
			obj = SimpleSmallInt(optr>>2)
		else:
			obj = self.parseObject(self.send(6,[optr&0xFF, optr>>8]))
		self.dm.releaseConnection()
		return obj

	def getCurCtx(self, nth=0):
		ctx = self.parseObject(self.send(4,[]))
		while isinstance(ctx, SimpleObj) and ctx.ptr!=NIL_PTR and nth>0:
			ctx = self.getObject(ctx.data[6])
			nth = nth - 1
		if (not isinstance(ctx, SimpleObj)) or ctx.ptr==NIL_PTR:
			return None
		return ctx
	
	def getRawGlobals(self):
		return self.parseObject(self.send(5,[]))
	
	def getGlobals(self):
		self.globals = self.parseDict(self.parseObject(self.send(5,[])))
		return self.globals

	def getGlobal(self, name):
		if not self.globals:
			self.getGlobals()
		if name in self.globals:
			return self.globals[name]
		else:
			return None

	def getLastError(self):
		self.dm.aquireConnection()
		x = self.send(7,[])
		errno = x[1]+(x[2]<<8)
		errparam = x[3]+(x[4]<<8)
		errstring = "Unknown error"
		if errno<len(errNames):
			errstring = errNames[errno]
		self.dm.releaseConnection()
		return (errno, errparam, errstring)

	def sendStaticRoots(self, data):
		self.send(8, data, showresp=0)

	def sendStaticBlock(self, addr, data):
		ln = len(data)/2
		addr /= 2
		cmd = [addr&0xFF, addr>>8, ln&0xFF, ln>>8]
		for d in data:
			cmd.append(d)
		self.send(9,cmd, showresp=0)

	def getVersion(self):
		self.dm.aquireConnection()
		data = self.send(11, [], showresp=0)
		self.dm.releaseConnection()
		str = ""
		for d in data[1:]:
			if d!=0:
				str += chr(d)
		return str.split("-")

	def getPlatform(self):
		self.dm.aquireConnection()
		data = self.send(12, [], showresp=0)
		self.dm.releaseConnection()
		str = ""
		for d in data[1:]:
			if d!=0:
				str += chr(d)
		return str.split("-")

	def setObjectData(self, optr, idx, val):
		data = [optr&0xFF, optr>>8, idx&0xFF, idx>>8, val&0xFF, val>>8]
		self.dm.aquireConnection()
		self.send(15, data, showresp=0)
		self.dm.releaseConnection()

	def allocObject(self, size, cls, bin=0):
		data = [size&0xFF, size>>8, cls&0xFF, cls>>8, bin]
		self.dm.aquireConnection()
		r = self.send(16, data, showresp=0)
		self.dm.releaseConnection()
		return self.parseObject(r)

	def allocSymbol(self, val):
		blen = len(val)
		sym = self.allocObject(blen, self.getGlobal("Symbol").ptr, 1)
		for i,c in enumerate(val):
			sym.setData(i, ord(c))
		return sym

	def allocByteArray(self, val):
		blen = len(val)
		sym = self.allocObject(blen, self.getGlobal("ByteArray").ptr, 1)
		for i,c in enumerate(val):
			sym.setData(i, c)
		return sym

	def runMethod(self, meth, recv):
		data = [meth&0xFF, meth>>8, recv&0xFF, recv>>8]
		self.dm.aquireConnection()
		self.send(17, data, showresp=0)
		self.dm.releaseConnection()

	def getLastReturn(self):
		self.dm.aquireConnection()
		r = self.send(18, [], showresp=0)
		self.dm.releaseConnection()
		return self.parseObject(r)

	def getBootState(self):
		self.dm.aquireConnection()
		s = self.send(131,[])
		self.dm.releaseConnection()
		if len(s)!=2:
			raise Exception("invalid boot state")
		return s[0] == 3 and s[1] == 1

	def isUpgradable(self):
		logging.error("implement: isUpgradable")
		return True

	def enterBootloader(self):
		logging.error("implement: enterBootloader")

	def bootInit(self):
		r = self.send(130,[], showresp=0)
		if r[0]!=1 or r[1]!=1:
			self.dm.releaseConnection()
			raise Exception("Couldn't initialise bootloader")

	def bootPageWrite(self, addr, data):
		di = 0
		for c in range(4):
			req = [addr&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF, (addr>>24)&0xFF]
			for i in range(32):
				req.append(data[di])
				di += 1
			r = self.send(129,req, showresp=0)
			if r[0]!=1 or (r[1]!=3 and r[1]!=1):
				self.dm.releaseConnection()
				raise Exception("Couldn't write page at 0x%06X" % addr)

	def bootImageWrite(self, start, data, progressCb=None):
		if (len(data)%128)!=0:
			x = ((len(data)/128)+1)*128 - len(data)
			for i in range(x):
				data.append(0)
		n = start
		end = start+len(data)
		while n < end:
			if progressCb:
				progressCb(int((n-start)/float(end-start)*100.0))
			r = self.bootPageWrite(n,data[n-start:n-start+128])
			n += 128
		if progressCb:
			progressCb(100)

	def upgrade(self, fn, progressCb=None):
		f = file(fn,"rb")
		data = map(ord,f.read())
		f.close()
		self.status.printNl("Upgrading firmware with '%s'" % fn)
		self.dm.aquireConnection()
		self.conn.setTimeout(1)
		self.enterBootloader()
		self.bootInit()
		self.bootImageWrite(0x2000, data, progressCb)
		self.conn.setTimeout(0.5)
		self.dm.releaseConnection()

	def upload(self, fn, progressCb=None):
		f = file(fn,"rb")
		data = map(ord,f.read())
		f.close()
		self.status.printNl("Uploading %s (%d bytes)." % (fn,len(data)))
		self.uploadCMG(data, progressCb)

	def uploadCMG(self, data, progressCb=None):
		self.dm.aquireConnection()
		try:
			self.sendStaticRoots(data[:34])
		except:
			self.status.printNl("Failed to upload static roots")
			return 
		statics = data[34:]
		for d in range(264 - len(statics)%264):
			statics.append(0xFF);
		remaining = len(statics)
		total = remaining
		blockSize = 44
		pos = 0
		try:
			while (remaining>=blockSize):
				if progressCb:
					progressCb(int((total-remaining)/float(total)*100.0))
				self.sendStaticBlock(pos,statics[pos:pos+blockSize])
				pos += blockSize
				remaining -= blockSize
			if remaining:
				self.sendStaticBlock(pos,statics[pos:pos+remaining])
			if progressCb:
				progressCb(100)
		except:
			self.status.printNl("Failed upload at %d" % pos)
			return
		#self.status.printNl("Upload complete.")
		self.dm.releaseConnection()
		self.restart()

	def arrayToPython(self, arr, limit=-1):
		arrPy = []
		array = arr.data
		if limit!=-1:
			array = array[:limit]
		for i,aptr in enumerate(array):
			item = self.getObject(aptr)
			if isinstance(item, SimpleSmallInt):
				arrPy.append(RemoteObj(item.val))
			else:
				acls = self.getObject(item.cls)
				aclsName = self.getObject(acls.data[0]).asString()
				if item.ptr == NIL_PTR:
					aclsName = "nil"
				elif aclsName == "String":
					aclsName = item.asString()
				elif aclsName == "Symbol":
					aclsName = "#%s" % item.asString()
				elif aclsName == "Integer":
					v = 0
					for i in range(3,-1,-1):
						v = (v << 8) | item.data[i]
					aclsName = "%d" % v
				else:
					aclsName = "<%s>" % aclsName
				arrPy.append(RemoteObj(aclsName, aptr))
		return arrPy

	def backtrace(self, verbose=False, mem=None):
		curCtx = self.getCurCtx()
		blkPtr = self.getGlobal("Block").ptr
		frames = []

		while curCtx.ptr != NIL_PTR:
			meth = self.getObject(curCtx.data[0])
			methName = self.getObject(meth.data[0]).asString()
			args = self.getObject(curCtx.data[1])
			temps = self.getObject(curCtx.data[2])
			bp = self.getObject(curCtx.data[4])
			inclsName = "?"
			numArgs = 0
			
			if curCtx.cls == blkPtr:
				ctxType = "block"
			else:
				ctxType = "message"
			tempArray = []
			if (temps.ptr != NIL_PTR):
				tempArray = self.arrayToPython(temps)
				ctxLine = 0 #"?"
				clsName = "?"
				if len(temps.data)>0:
					arg = self.getObject(temps.data[0])
					if isinstance(arg, SimpleSmallInt):
						mcls = "SmallInt"
					else:
						acls = self.getObject(arg.cls)
						mcls = self.getObject(acls.data[0]).asString()
					bpv = 0
					if isinstance(bp, SimpleSmallInt):
						bpv = bp.val
					else:
						logging.error("bad bytepointer in backtrace %s" % bp)
					clsName = mcls
					inclsName = mcls
					numArgs = len(tempArray)
					if mem:
						scls = mem.getGlobal(mcls)
						if (scls):
							# this gets broken by doit currently (because the receiver isn't set correctly)
							smeth = scls.getMethod(methName)
							if smeth:
								inclsName = smeth.incls.name
								ctxLine = smeth.lineFromBp(bpv)
								numArgs = len(smeth.args)
				else:
					break
				tempsArray = tempArray[numArgs:]
				argsArray = tempArray[:numArgs]
				f = RemoteStackFrame(
						methName, inclsName, clsName, ctxLine, ctxType, argsArray, tempsArray)
				frames.append(f)
			curCtx = self.getObject(curCtx.data[6])
		return frames
	
	def rawRead(self, addr, ln):
		cmd = [addr&0xFF, addr>>8, ln&0xFF, ln>>8]
		return self.send(10,cmd, showresp=0)
